Skip to content

fix(pricing): generate the embedded price table from the live feed - #13

Merged
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-564-generated-price-defaults
Aug 2, 2026
Merged

fix(pricing): generate the embedded price table from the live feed#13
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-564-generated-price-defaults

Conversation

@hallelx2

@hallelx2 hallelx2 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Why

The embedded table answers every lookup the override and remote layers don't: UseRemote off, network down, or a first refresh still in flight. Hand-maintained and dated "as of April 2026", it had drifted:

model was actual error
gemini-2.5-flash 0.15 / 0.60 0.30 / 2.50 output at 0.24x
gpt-4o 2.50 / 10.00 2.75 / 11.00 0.91x
Claude 5 / 4.6 family absent priced upstream unpriced offline

Under-reporting is the dangerous direction: it silently defeats budget caps and biases quality-per-dollar toward whichever model's price is stalest.

What

defaultPrices moves to defaults_gen.go, generated by pricing/gen from the LiteLLM feed via go generate ./pricing.

The generator doesn't reimplement rate selection — it installs a real snapshot and reads it back through pricing.Lookup, so vendor precedence (#12), dropped rows and cache backfill apply exactly as at runtime. The embedded table cannot diverge from the rules the remote layer follows.

Two more defects this uncovered

A 10x over-report. Canonical() reduced anthropic.claude-v1 and claude-v2:1 to a bare claude at Claude 1's 8.00/24.00. longestPrefix matches at segment boundaries, so that key answered for every Claude model not listed explicitly — claude-haiku-3-5 resolved to 8.00/24.00 against its real 0.80/4.00. An ID that has lost all version information is a family name, not a model, so the qualified form is kept. (This also stops the legacy entries collapsing together: the snapshot now holds 1086 models, up from 1070.)

A silent prefix fallback in the generator. claude-haiku-3-5 isn't an upstream ID — Anthropic put the generation before the tier on the 3.x line (claude-3-5-haiku) and after it from 4.x on. Lookup happily prefix-matched it. The generator now requires an exact key in the snapshot, so a curated ID that quietly goes missing fails the build rather than inheriting a sibling's price; an explicit alias() keeps llmgate's public ID stable.

Staying current

A weekly workflow regenerates and fails if the committed table moved — the signal that a vendor changed a price. The generator leaves the file alone when only its own timestamp would change, so the job fires on real drift rather than every Monday. Verified: go generate twice in a row leaves git diff clean.

Tests

  • TestFamilyNameIsNotAModel — the 8.00/24.00 regression.
  • TestGeneratedDefaultsAreSane — ten rates pinned to vendor list prices.
  • TestNoAbsurdDefaults — units-error guard across every known model, including "a cache hit is never dearer than an uncached token".
  • TestDefaultsAsOfIsParseable — the new DefaultsAsOf() provenance stamp.

Full suite green locally.

Closes HAL-564

Summary by Sourcery

Generate the compiled-in pricing defaults from the live LiteLLM feed and harden pricing provenance and model ID handling.

New Features:

  • Add a generator that rebuilds the embedded default price table from the LiteLLM feed via go generate.
  • Expose DefaultsAsOf to report when the compiled-in price table was last generated.
  • Introduce a scheduled CI workflow that detects drift between the embedded price table and upstream prices.

Bug Fixes:

  • Prevent canonicalisation from collapsing Claude model IDs into the bare family name, avoiding incorrect prefix-based pricing for unlisted models.
  • Ensure curated model IDs used in the generator must exist exactly in the upstream snapshot to avoid silent prefix fallbacks and mispricing.

Enhancements:

  • Move default pricing data into a generated defaults_gen.go file that aligns with runtime lookup rules and vendor precedence.
  • Add tests to validate generated defaults against vendor list prices, guard against implausible rates, and verify the provenance stamp is parseable.

CI:

  • Add a weekly price-drift workflow that runs the generator and fails when the embedded table no longer matches the live feed.

The embedded table answers every lookup the override and remote layers
do not: UseRemote off, network down, or a first refresh still in flight.
Hand-maintained and dated "as of April 2026", it had drifted.
gemini-2.5-flash carried an output rate of 0.60 against a real 2.50 —
under-reporting spend on that model by more than 4x, in the direction
that silently defeats budget caps. The Claude 5 and 4.6 families were
missing outright, so an offline caller running them got nothing at all.

defaultPrices now lives in defaults_gen.go, produced by pricing/gen from
the LiteLLM feed. Rather than reimplement rate selection, the generator
installs a real snapshot and reads it back through pricing.Lookup, so
vendor precedence, dropped rows and cache backfill apply exactly as they
do at runtime and the two cannot diverge.

Regenerating uncovered two further defects, both fixed here.

Canonical() reduced "anthropic.claude-v1" and "claude-v2:1" to a bare
"claude" at Claude 1's 8.00/24.00. longestPrefix matches at segment
boundaries, so that key then answered for every Claude model not listed
explicitly: claude-haiku-3-5 resolved to 8.00/24.00 against its real
0.80/4.00, a 10x over-report. An ID that has lost all version
information is a family name, not a model, so the qualified form is kept
instead. This also stops the legacy entries collapsing together, which
is why the snapshot now holds 1086 models rather than 1070.

The curated list needed claude-haiku-3-5 sourced from upstream's
claude-3-5-haiku — Anthropic put the generation before the tier on the
3.x line and after it from 4.x on. The generator now requires an exact
key in the snapshot rather than accepting whatever Lookup's prefix
fallback returns, so a curated ID that has quietly gone missing fails
the build instead of inheriting a sibling's price.

Rates are rounded to ten decimal places on the way out. Upstream quotes
per-token, so every value arrives multiplied by a million and 2e-07
renders as 0.19999999999999998, which is unreadable and makes each
regeneration a noisy diff.

A weekly workflow regenerates and fails if the committed table has
moved, which is the signal that a vendor changed a price. The generator
leaves the file untouched when only its own timestamp would change, so
that job fires on real drift rather than every Monday.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hallelx2, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fe4ff0e-397a-4e73-a06a-d17761b30b80

📥 Commits

Reviewing files that changed from the base of the PR and between d6c907d and 76f4687.

⛔ Files ignored due to path filters (1)
  • pricing/gen/main.go is excluded by !**/gen/**
📒 Files selected for processing (5)
  • .github/workflows/price-drift.yml
  • pricing/canonical.go
  • pricing/defaults_gen.go
  • pricing/defaults_test.go
  • pricing/pricing.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR replaces the hand-maintained embedded pricing table with a generated snapshot from the live LiteLLM feed, fixes a canonicalization bug that over-reported Claude family prices, adds provenance for compiled-in prices, and introduces tests and CI to keep the defaults table correct and current.

Sequence diagram for the new pricing generator using the live LiteLLM feed

sequenceDiagram
    participant Gen as gen_main
    participant Pricing as pricing_pkg
    participant LiteLLM as LiteLLMSource

    Gen->>Pricing: UseRemote(ctx, RemoteConfig{Sources: [LiteLLMSource], RefreshInterval: -1})
    Note over Pricing,LiteLLM: LiteLLMSource installs snapshot into remote layer

    Gen->>Pricing: AsOf()
    Pricing-->>Gen: asOf, source

    Gen->>Pricing: KnownModels()
    Pricing-->>Gen: []modelID

    loop curated models
        Gen->>Gen: source() // choose id or alias from
        Gen->>Gen: present[modelID]
        alt [modelID present]
            Gen->>Pricing: Lookup(modelID)
            Pricing-->>Gen: Price, ok
            alt [ok]
                Gen->>Gen: prices[id] = Price
            else [!ok]
                Gen->>Gen: missing = append(missing, modelID)
            end
        else [modelID missing]
            Gen->>Gen: missing = append(missing, modelID)
        end
    end

    alt [missing not empty]
        Gen->>Gen: return error
    else [all present]
        Gen->>Gen: render(prices, asOf, source)
        Gen->>Gen: sameRates(old, new)
        alt [rates unchanged]
            Gen->>Gen: leave defaults_gen.go
        else [rates changed]
            Gen->>Gen: write defaults_gen.go
        end
    end
Loading

Flow diagram for updated Canonical model ID normalization

flowchart TD
    A[Input model ID] --> B[stripGatewayPrefix]
    B --> C[stripRegionPrefix]
    C --> D[stripPrefix]
    D --> E[stripProviderPrefix]
    E --> F[lowercase]
    F --> G[strip versionSuffix]
    G --> H[strip dateSuffix]
    H --> I{contains any digit?}
    I -->|yes| J[return unqualified ID]
    I -->|no| K[return qualified ID]

    subgraph Legend
      direction LR
      L1[qualified = ID after provider/gateway/region stripping]
      L2[unqualified = ID after version/date stripping]
    end
Loading

File-Level Changes

Change Details Files
Embed default price table via generated defaults_gen.go fed from LiteLLM through pricing.Lookup instead of a manually maintained map.
  • Remove hard-coded defaultPrices map from pricing.go and replace it with a go:generate directive pointing to pricing/gen.
  • Implement pricing/gen command that installs a LiteLLM remote snapshot via pricing.UseRemote, validates curated model IDs against the snapshot, and renders defaults_gen.go using pricing.Lookup for rate selection.
  • Ensure generator only rewrites defaults_gen.go when actual rates change by comparing generated output to existing file while ignoring provenance stamps.
  • Introduce curated model list (Anthropic, OpenAI, Google, Zhipu/GLM families) and alias handling for mismatched upstream IDs (e.g., claude-haiku-3-5 vs claude-3-5-haiku).
  • Add num() helper to normalize floating-point rates to clean decimals to avoid noisy diffs.
pricing/pricing.go
pricing/gen/main.go
pricing/defaults_gen.go
Fix Canonical model ID handling so family names (e.g., "claude") don’t act as catch-all prefixes that misprice models.
  • Capture the fully qualified ID before stripping version and date suffixes in Canonical.
  • After stripping qualifiers, detect IDs with no digits and return the qualified form instead of the stripped family name to prevent over-broad prefix matches.
  • Document the 10x over-report regression scenario in canonical.go comments for future maintainers.
pricing/canonical.go
Expose provenance of compiled-in prices via DefaultsAsOf and add tests that validate generated defaults and guard against pricing/unit errors.
  • Add DefaultsAsOf function that parses a RFC3339 timestamp defaultsAsOf from the generated file, returning zero time on parse failure.
  • Create defaults_test.go with tests: regression for family-name canonicalization, sanity checks for specific key rates against vendor list prices, guardrails against absurd rates or cache reads more expensive than inputs, and validation that DefaultsAsOf returns a reasonable timestamp.
  • Wire defaultsAsOf variable into generated defaults_gen.go for provenance and validate via tests.
pricing/pricing.go
pricing/defaults_gen.go
pricing/defaults_test.go
Introduce a scheduled GitHub Actions workflow to detect price drift between the embedded table and the live feed.
  • Add price-drift.yml workflow that runs weekly and on manual dispatch, executes go generate ./pricing, and fails if pricing/defaults_gen.go has changed.
  • Document in the workflow that failures are signals of vendor price changes requiring regeneration and review, not broken builds.
  • Use actions/setup-go with Go 1.25 and enable module cache for the drift job.
.github/workflows/price-drift.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@hallelx2
hallelx2 merged commit 0c4550f into main Aug 2, 2026
7 checks passed
@hallelx2
hallelx2 deleted the halleluyaholudele/hal-564-generated-price-defaults branch August 2, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant